﻿using System;
using System.Configuration;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;

namespace EnterpriseLibraryExtensions.Unity.ServiceModel
{
    /// <summary>
    /// This class enabled Windows Communication Foundation to initialize services using Unity and thereby
    /// enabling dependency injection.
    /// </summary>
    public class UnityInstanceProvider : IInstanceProvider
    {
        /// <summary>
        /// Name of the instanceContext parameter.
        /// </summary>
        internal const string InstanceContextParameter = "instanceContext";

        /// <summary>
        /// The name of the Unity container to use (if set).
        /// </summary>
        private readonly string _containerName;

        /// <summary>
        /// Should the container to use be the global container.
        /// </summary>
        private readonly bool _useEnterpriseLibraryContainer;

        /// <summary>
        /// The name of the unity section defined in the configuration file.
        /// </summary>
        private string _unitySectionName;

        /// <summary>
        /// Initializes a new instance of the UnityInstanceProvider class and associates it with a Unity container.
        /// </summary>
        /// <param name="containerName">The name of the Unity container. If passed null or empty, the default container will be used.</param>
        public UnityInstanceProvider(string containerName)
        {
            _containerName = containerName;
        }

        /// <summary>
        /// Initializes a new instance of the UnityInstanceProvider class and makes it possible to specify whether or 
        /// not to use the Enterprise Library's global container specified on <see cref="EnterpriseLibraryContainer.Current"/>. 
        /// Specifying false will initialize services using the default container from the configuration 
        /// (same as calling the constructor with an empty container name).
        /// </summary>
        /// <param name="useEnterpriseLibraryContainer">Pass true to make use of the global provider.</param>
        public UnityInstanceProvider(bool useEnterpriseLibraryContainer)
        {
            _useEnterpriseLibraryContainer = useEnterpriseLibraryContainer;
        }

        /// <summary>
        /// Gets a value indicating whether the global container is in use on this provider.
        /// </summary>
        internal bool UsesEnterpriseLibraryContainer
        {
            get { return _useEnterpriseLibraryContainer; }
        }

        /// <summary>
        /// Returns a service object given the specified <see cref="T:System.ServiceModel.InstanceContext"/> object.
        /// </summary>
        /// <param name="instanceContext">The current <see cref="T:System.ServiceModel.InstanceContext"/> object.</param>
        /// <param name="message">The message that triggered the creation of a service object.</param>
        /// <returns>The service object.</returns>
        /// <exception cref="System.ArgumentNullException">Thrown if instanceContext is null.</exception>
        public object GetInstance(InstanceContext instanceContext, Message message)
        {
            if (instanceContext == null)
            {
                throw new ArgumentNullException(InstanceContextParameter);
            }

            IUnityContainer container = _useEnterpriseLibraryContainer ? GetEnterpriseLibraryUnityContainer() : CreateContainerFromConfiguration();
            return container.Resolve(instanceContext.Host.Description.ServiceType);
        }

        /// <summary>
        /// Returns a service object given the specified <see cref="T:System.ServiceModel.InstanceContext"/> object.
        /// </summary>
        /// <param name="instanceContext">The current <see cref="T:System.ServiceModel.InstanceContext"/> object.</param>
        /// <returns>A user-defined service object.</returns>
        /// <exception cref="System.ArgumentNullException">Thrown if instanceContext is null.</exception>
        public object GetInstance(InstanceContext instanceContext)
        {
            return GetInstance(instanceContext, null);
        }

        /// <summary>
        /// Called when an <see cref="T:System.ServiceModel.InstanceContext"/> object recycles a service object.
        /// </summary>
        /// <param name="instanceContext">The service's instance context.</param>
        /// <param name="instance">The service object to be recycled.</param>
        public void ReleaseInstance(InstanceContext instanceContext, object instance)
        {
        }

        /// <summary>
        /// Gets the EnterpriseLibraryContainer's current service locator and resolves the Unity container.
        /// </summary>
        /// <returns>The Unity container currently used by Enterprise Library.</returns>
        internal virtual IUnityContainer GetEnterpriseLibraryUnityContainer()
        {
            return EnterpriseLibraryContainer.Current.GetInstance<IUnityContainer>();
        }

        /// <summary>
        /// Initializes a Unity container by configuration.
        /// </summary>
        /// <returns>A unity container instance.</returns>)]
        internal virtual IUnityContainer CreateContainerFromConfiguration()
        {
            IUnityContainer container = new UnityContainer();

            UnityConfigurationSection section = GetUnityConfigurationSection();

            if (section == null)
            {
                throw new InvalidOperationException(Properties.Resources.UnitySectionNotFound);
            }

            if (!section.Containers.Any(c => c.Name == _containerName))
            {
                throw new InvalidOperationException(Properties.Resources.UnityContainerNotFound);
            }

            if (string.IsNullOrEmpty(_containerName))
            {
                section.Configure(container);
            }
            else
            {
                section.Configure(container, _containerName);
            }

            return container;
        }

        /// <summary>
        /// Gets the unity configuration section in the executables config file.
        /// </summary>
        /// <returns>A UnityConfigurationSection.</returns>
        /// <exception cref="System.InvalidOperationException">Thrown if the UnityConfigurationSection is not present in the config file.</exception>
        internal virtual UnityConfigurationSection GetUnityConfigurationSection()
        {
            if (string.IsNullOrEmpty(_unitySectionName))
            {
                var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
                foreach (ConfigurationSection sectionConfiguration in config.Sections)
                {
                    if (sectionConfiguration.ElementInformation.Type.Name == "UnityConfigurationSection")
                    {
                        _unitySectionName = sectionConfiguration.SectionInformation.Name;
                        break;
                    }
                }

                if (string.IsNullOrEmpty(_unitySectionName))
                {
                    throw new InvalidOperationException("Unity section not found in configuration");
                }
            }

            return (UnityConfigurationSection)ConfigurationManager.GetSection(_unitySectionName);
        }
    }
}
